查看原文
其他

在 Node.js 中使用 Promise.prototype.finally

前端大全 2022-06-29

(点击上方公众号,可快速关注)


英文: Valeri Karpov  译文:众成翻译/AlekoLau

zcfy.cc/article/using-promise-prototype-finally-in-node-js

Promise.prototype.finally()  最近达到了 TC39 提案的 第 4 阶段 。这意味着 Promise.prototype.finally() 提案被采纳成为 ECMAScript 最新特性草案 的一部分,登陆 Node.js 现在只是时间问题了。这篇文章会向大家展示 Promise.prototype.finally() 的用法和简化版 Polyfill 的写法。

Promise.prototype.finally() 是什么?


假设你创建了一个新的 Promise:

const promiseThatFulfills = new Promise((resolve) => {

  // 调用 resolve() 可以让 Promised 的状态变为 fulfilled。"fulfilled" 和 "resolved" 是不同的概念:

  // 如果你 resolve() 一个非 Promise 值,Promise 会变成 "fulfilled"。

  // 然而, 如果 resolve() 一个 Promise,外层(原来的) Promise 会保持 "pending" 状态

  // 直到内层 Promise 变为 "fulfilled" 或者 "rejected"

  setTimeout(() => resolve('Hello, World'), 1000);

});


const promiseThatRejects = new Promise((resolve, reject) => {

  setTimeout(() => reject(new Error('whoops!')), 1000);

});

你可以用 .then() 函数把这些 Promise 串联在一起。

promiseThatFulfills.then(() => console.log('Will print after about 1 second'));

promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));

注意 .then() 需要两个函数作为参数。第一个参数是 onFulfilled(),当 Promise 为 fulfilled 时调用;第二个 onRejected() 则是在 rejected 的时候调用。Promise 是一个必定处于以下三种状态之一的状态机:

  • pending(进行中): Promise 中的操作正在进行中,状态未被凝固为 fulfilled 或 rejected。

  • fulfilled(已完成,直译:已满足): Promise 中的操作已成功完成,现在 Promise 里面关联有该操作的返回值。

  • rejected(已失败,直译:已回绝): Promise 中的操作因某些原因失败,现在 Promise 里面关联有该操作的错误信息。

此外,处于 fulfilled 或者 rejected 状态的 Promise 称作“已凝固”(settled) 的 Promise。

虽然 .then() 是串联 Promise 的核心机制,但并不独一无二。Promise 用来处理抛出错误的 .catch() 函数 也能串联 Promise。

promiseThatRejects.catch(() => console.log('Will print after about 1 second'));

.catch() 函数只是一个只有 onRejected() 参数的 .then() 的语法糖:

promiseThatRejects.catch(() => console.log('Will print after about 1 second'));

// 等价于

promiseThatRejects.then(null, () => console.log('Will print after about 1 second'));

类似于 .catch(),.finally() 也是 .then() 的一个语法糖。区别在于 .finally() 当 Promise 凝固(fulfilled / rejected)时执行一个 onFinally 函数。当前 .finally() 还没有加入 Node.js 发行版,但 npm 上的 promise.prototype.finally 模块 实现了它的 Polyfill。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


const promiseThatFulfills = new Promise((resolve) => {

  setTimeout(() => resolve('Hello, World'), 1000);

});


const promiseThatRejects = new Promise((resolve, reject) => {

  setTimeout(() => reject(new Error('whoops!')), 1000);

});


promiseThatFulfills.finally(() => console.log('fulfilled'));

promiseThatRejects.finally(() => console.log('rejected'));

上面代码的运行结果会打印 'fulfilled' 和 'rejected',因为无论是 fulfilled 还是 rejected,只要状态凝固 onFinally 都会立即执行。不过 onFinally 接受参数,所以你无法判断 Promise 的状态到底是两个中的哪个。

finally() 会返回一个 Promise,所以你可以使用 .then() / .catch() / .finally() 串联它的返回值。finally() 返回的 Promise 会和它连接到的 Promise 保持相同的 fulfill 条件。 例如下面的代码,即使 onFinally 返回了 'bar',它还是会打印 5 次 'foo' 。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


Promise.resolve('foo').

  finally(() => 'bar').

  // 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用

  // for fulfilled values and rejected errors

  then(res => console.log(res));

类似地,下面代码中即使 onFinally 没有抛出任何错误,仍然会打印 'foo'。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


Promise.reject(new Error('foo')).

  finally(() => 'bar').

  // 会打印 'foo', **不是** 'bar',因为 finally() 只起到转运的作用

  // 无论是 resolve 的值还是 reject 的错误

  catch(err => console.log(err.message));

上面代码展示了使用 finally() 的一个重要细节:它 不会 帮你处理 Promise 的错误。如何让它能处理 Promise 错误值得更深入的研究。

错误处理


finally() 不是 用来处理 Promise 的错误的。事实上,它会在 onFinally() 执行后显式重新抛错。下面的代码会打印一个未被处理的 Promise 错误警告。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


const promiseThatRejects = new Promise((resolve, reject) => {

  setTimeout(() => reject(new Error('whoops!')), 1000);

});


promiseThatRejects.finally(() => console.log('rejected'));

$ node finally.js

rejected

(node:5342) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): Error: whoops!

(node:5342) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

$

与 try/catch/finally 类似,通常 .finally() 都会在 .catch() 后面被调用。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


const promiseThatRejects = new Promise((resolve, reject) => {

  setTimeout(() => reject(new Error('whoops!')), 1000);

});


promiseThatRejects.

  catch(() => { /* ignore the error */ }).

  finally(() => console.log('done'));

然而 finally() 返回的也是 Promise,所以你可以随意在 finally() 后面调用 .catch()。特别地,如果 onFinally 会出错,例如 HTTP 请求,你应该在末尾添加 .catch() 以处理可能发生的错误。

const promiseFinally = require('promise.prototype.finally');


// 向 Promise.prototype 增加 finally()

promiseFinally.shim();


const promiseThatRejects = new Promise((resolve, reject) => {

  setTimeout(() => reject(new Error('whoops!')), 1000);

});


promiseThatRejects.

  finally(() => console.log('rejected')).

  // No unhandled promise rejection because there's a .catch()

  catch(() => { /* ignore the error */ });


简版 Polyfill


我觉得想要真正搞懂一个东西,最简单的方式就是自己去实现一个。.finally() 是一个很好的选择,因为官方 Polyfill 只有 45 行,而且大多数代码在验证原理时可以进一步精简。

接下来是一些关于 .finally() 的测试样例。下面的代码会打印 'foo' 5 次。

// 返回值被忽略,Promise 正常完成

Promise.resolve('foo').

  finally(() => 'bar').

  then(res => console.log(res));


// 返回值被忽略,Promise 正常抛错

Promise.reject(new Error('foo')).

  finally(() => 'bar').

  catch(err => console.log(err.message));


// onFinally 抛错,返回新抛出的错误

Promise.reject(new Error('bar')).

  finally(() => { throw new Error('foo'); }).

  catch(err => console.log(err.message));


// onFinally 返回的是一个抛错的 Promise,

// 返回新抛出的错误

Promise.reject(new Error('bar')).

  finally(() => Promise.reject(new Error('foo'))).

  catch(err => console.log(err.message));


// onFinally 返回的是一个 Promise, 需要等待它

// 状态凝固才能继续执行

const start = Date.now();

Promise.resolve('foo').

  finally(() => new Promise(resolve => setTimeout(() => resolve(), 1000))).

  then(res => console.log(res, Date.now() - start));

下面是简版 Polyfill 的实现。

// 向 Promise.prototype 增加 finally()

Promise.prototype.finally = function(onFinally) {

  return this.then(

    /* onFulfilled */

    res => Promise.resolve(onFinally()).then(() => res),

    /* onRejected */

    err => Promise.resolve(onFinally()).then(() => { throw err; })

  );

};

这个实现背后关键的思路在于 onFinally 可能返回 Promise。在这种情况下你需要用 .then() 来处理它并且给外层 Promise 凝固状态。你可以显式检查 onFinally 是否返回 Promise,但 Promise.resolve() 已经帮你做了,而且不需要 if 语句。你还需要跟踪初始 Promise 的值或错误,并确保 finally() 返回的 Promise 解析出初始值 res,或重新抛出初始错误 err。

后记


在动笔时,Promise.prototype.finally() 是 8 个 TC39 第四阶段提案 之一。这意味着 finally() 将和 7 个其他新语言特性一起加入 Node.js。 finally() 是这 8 个新特性中最令人兴奋的之一,皆因为它可以让异步操作结束后的清理更彻底。举个例子,下面我正用在生产环境的代码非常需要 finally() 来在函数完成时释放资源的锁定。


【关于投稿】


如果大家有原创好文投稿,请直接给公号发送留言。


① 留言格式:
【投稿】+《 文章标题》+ 文章链接

② 示例:
【投稿】《不要自称是程序员,我十多年的 IT 职场总结》:http://blog.jobbole.com/94148/

③ 最后请附上您的个人简介哈~




觉得本文对你有帮助?请分享给更多人

关注「前端大全」,提升前端技能

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存